﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EFContrib.PS4EF.Tst.RelationShipType;
using System.Data.Objects.DataClasses;
using ComplexType = EFContrib.PS4EF.Tst.ComplexType;
using System.Diagnostics;
using OneSimpleType = EFContrib.PS4EF.Tst.OneSimpleType;
using EditableBusinessObjects.Postsharp.Public;
using System.Data.Objects;
using System.Data;
using System.Data.Common;
using System.Data.Metadata.Edm;
using System.Reflection;
using System.IO;
using System.Xml;
using System.Runtime.Serialization;
using CyclicReferences.Public;
using EntityFrameworkContrib.PostSharp4EF;

namespace EFContrib.PS4EF.TestingApp
{
    class Program
    {
        static void Main(string[] args)
        {
            ComplexType.Person p = new ComplexType.Person();
            IUpdateComplexTypesWithTracker u = p as IUpdateComplexTypesWithTracker;

            // a bug in visual studio keeps us from running tests (names are too long!)
            // http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=610131&SiteID=1
            // that is why there is an application here

            Console.WriteLine("(r) for relationshipDemo, (c) for complexTypesDemo, (t) for test");
            Char input = Console.ReadKey().KeyChar;

            switch (input)
            {
                case ('r'):
                    RelationShipDemo();
                    break;
                case ('c'):
                    ComplexTypesDemo();
                    break;
                case ('t'):
                    Test();
                    break;
            }
        }

        private static void Test()
        {
            using (OneSimpleType.OneSimpleTypeConnection context = new OneSimpleType.OneSimpleTypeConnection())
            {
                // clear out database
                foreach (OneSimpleType.Person old in context.Person)
                {
                    context.DeleteObject(old);
                }
                context.SaveChanges();
            }

            OneSimpleType.Person p = new OneSimpleType.Person { Firstname = "Ruurd", Lastname = "Boeke" };
            IEditableBusinessObject ep = p as IEditableBusinessObject;
            string MsgOnWire = "";

            using (OneSimpleType.OneSimpleTypeConnection context = new OneSimpleType.OneSimpleTypeConnection())
            {
                context.AddToPerson(p);
                context.SaveChanges();  // at this point, there should be 'original values'

                // the server is changing the object without yet saving
                p.Firstname = "ServerChanged";

                // now we need to create a version of the object that can be serialized well
                p = (OneSimpleType.Person)context.CreateSerializableVersion(p);

                DataContractSerializer s = new DataContractSerializer(p.GetType(), null, int.MaxValue, false, false, new SubstituteDomainDataContractSurrogate());

                MsgOnWire = GetWellFormedToContract(p, s);

            }

            // imagine we are the client getting a message and deserializing it
            DataContractSerializer s2 = new DataContractSerializer(typeof(OneSimpleType.Person), null, int.MaxValue, false, false, new SubstituteDomainDataContractSurrogate());
            StringReader reader = new StringReader(MsgOnWire);
            XmlReader xreader = XmlReader.Create(reader);
            OneSimpleType.Person p2 = (OneSimpleType.Person)s2.ReadObject(xreader);

            // edit something on the client
            p2.Lastname = "ClientChanged";


            // we have edited everything, let's get back to the server
            s2 = new DataContractSerializer(typeof(OneSimpleType.Person), null, int.MaxValue, false, false, new SubstituteDomainDataContractSurrogate());
            MsgOnWire = GetWellFormedToContract(p2, s2);

            // we're at the server
            using (OneSimpleType.OneSimpleTypeConnection context = new OneSimpleType.OneSimpleTypeConnection())
            {
                DataContractSerializer s3 = new DataContractSerializer(typeof(OneSimpleType.Person), null, int.MaxValue, false, false, new SubstituteDomainDataContractSurrogate());
                StringReader reader3 = new StringReader(MsgOnWire);
                XmlReader xreader3 = XmlReader.Create(reader3);
                OneSimpleType.Person p3 = (OneSimpleType.Person)s3.ReadObject(xreader3);

                ((IPocoFacade)p3).AttachGraphToContext(context, delegate(object source) { return "Person"; });
                

                context.SaveChanges();
            }


        }


        private static void ComplexTypesDemo()
        {
            using (ComplexType.ComplexTypesTestEntities context = new ComplexType.ComplexTypesTestEntities())
            {
                // clear out database
                foreach (ComplexType.Person old in context.Person)
                {
                    context.DeleteObject(old);
                }
                context.SaveChanges();

                ComplexType.Person p = new ComplexType.Person { FirstName = "Ruurd", LastName = "Boeke" };

                // this will set the changetracker
                context.AddToPerson(p);

                ComplexType.Address a = new ComplexType.Address { City = "Rotterdam", Street = "My Street", PostalCode = "1111 VA" };
                ComplexType.NestedComplexType n = new ComplexType.NestedComplexType { A = 1, B = 2 };
                a.NestedComplexType = n;
                p.Address = a;

                IGetChangeTracker ctA = PostSharp.Post.Cast<ComplexType.Address, IGetChangeTracker>(a);
                Debug.Assert(ctA.GetChangeTracker() != null);

                ComplexType.Address b = new ComplexType.Address { City = "Seattle", Street = "redmond street", PostalCode = "2222 BB" };
                b.NestedComplexType = n;
                p.Address = b;
                Debug.Assert(ctA.GetChangeTracker() == null);

                IGetChangeTracker ctB = PostSharp.Post.Cast<ComplexType.Address, IGetChangeTracker>(b);
                Debug.Assert(ctB.GetChangeTracker() != null);

                context.SaveChanges();
            }
        }

        private static void RelationShipDemo()
        {
            using (SimpleRelationshipTestEntities context = new SimpleRelationshipTestEntities())
            {

                foreach (Car oldCar in context.Car)
                {
                    IRelationshipLoader noLazyLoading = PostSharp.Post.Cast<Car, IRelationshipLoader>(oldCar);

                    bool wasLoaded = noLazyLoading.IsLoaded("Customer");

                    EntityReference<Customer> cu = noLazyLoading.GetRelatedReference<Customer>("Customer");
                    cu.Load();

                    wasLoaded = noLazyLoading.IsLoaded("Customer");
                }



                // clear out database
                foreach (Customer old in context.Customer)
                {
                    IRelationshipLoader noLazyLoading = PostSharp.Post.Cast<Customer, IRelationshipLoader>(old);

                    bool wasLoaded = noLazyLoading.IsLoaded("Orders");

                    noLazyLoading.Load("Orders");
                    wasLoaded = noLazyLoading.IsLoaded("Orders");

                    EntityCollection<Order> orders = noLazyLoading.GetRelatedCollection<Order>("Orders");

                    context.DeleteObject(old);
                }
                context.SaveChanges();


                Customer c = new Customer { Name = "Ruurd Boeke" };
                Customer c2 = new Customer { Name = "Test Customer" };
                Car car = new Car { Make = "Ferrari" };
                Order o1 = new Order { Amount = 10 };
                Order o2 = new Order { Amount = 20 };
                Order o3 = new Order { Amount = 30 };

                car.Customer = c;
                car.Customer = c2;  // can re assign test

                // add customer on order
                o1.Customer = c;
                o2.Customer = c;
                // add order on customer, new way
                c.Orders.Add(o3);



                context.AddToCustomer(c);
                context.AddToCustomer(c2);
                context.SaveChanges();
            }
        }

        private static string GetWellFormedToContract(object p, DataContractSerializer s)
        {
            MemoryStream m = new MemoryStream();
            XmlTextWriter tw = new XmlTextWriter(m, Encoding.UTF8);
            tw.Formatting = Formatting.Indented;
            tw.Indentation = 4;
            tw.IndentChar = ' ';

            s.WriteObject(tw, p);
            tw.Flush();

            m.Position = 0;
            StreamReader sr = new StreamReader(m);
            string strOutput = sr.ReadToEnd();

            return strOutput;
        }


    }

    public static class UtilityExtensionMethods
    {
        #region daniels
		
        //
        // just type a little less
        //

        public static EntityState GetEntityState(this ObjectContext context, EntityKey key)
        {
            var entry = context.ObjectStateManager.GetObjectStateEntry(key);
            return entry.State;
        }

        public static string GetFullEntitySetName(this EntityKey key)
        {
            return key.EntityContainerName + "." + key.EntitySetName;
        }

        public static IEntityWithKey GetEntityByKey(this ObjectContext context, EntityKey key)
        {
            return (IEntityWithKey)context.ObjectStateManager.GetObjectStateEntry(key).Entity;
        }

        //
        // ObjectStateEntry
        //

        public static IExtendedDataRecord UsableValues(this ObjectStateEntry entry)
        {
            switch (entry.State)
            {
                case EntityState.Added:
                case EntityState.Detached:
                case EntityState.Unchanged:
                case EntityState.Modified:
                    return (IExtendedDataRecord)entry.CurrentValues;
                case EntityState.Deleted:
                    return (IExtendedDataRecord)entry.OriginalValues;
                default:
                    throw new InvalidOperationException("This entity state should not exist.");
            }
        }

        public static EdmType EdmType(this ObjectStateEntry entry)
        {
            return entry.UsableValues().DataRecordInfo.RecordType.EdmType;
        }

        public static bool IsManyToMany(this AssociationType associationType)
        {
            foreach (RelationshipEndMember endMember in associationType.RelationshipEndMembers)
            {
                if (endMember.RelationshipMultiplicity != RelationshipMultiplicity.Many)
                {
                    return false;
                }
            }
            return true;
        }

        //
        // RelationshipEntry
        //

        public static bool IsRelationshipForKey(this ObjectStateEntry entry, EntityKey key)
        {
            if (entry.IsRelationship == false)
            {
                return false;
            }
            return ((EntityKey)entry.UsableValues()[0] == key) || ((EntityKey)entry.UsableValues()[1] == key);
        }

        public static EntityKey OtherEndKey(this ObjectStateEntry relationshipEntry, EntityKey thisEndKey)
        {
            Debug.Assert(relationshipEntry.IsRelationship);
            Debug.Assert(thisEndKey != null);

            if ((EntityKey)relationshipEntry.UsableValues()[0] == thisEndKey)
            {
                return (EntityKey)relationshipEntry.UsableValues()[1];
            }
            else if ((EntityKey)relationshipEntry.UsableValues()[1] == thisEndKey)
            {
                return (EntityKey)relationshipEntry.UsableValues()[0];
            }
            else
            {
                throw new InvalidOperationException("Neither end of the relationship contains the passed in key.");
            }
        }

        public static string OtherEndRole(this ObjectStateEntry relationshipEntry, EntityKey thisEndKey)
        {
            Debug.Assert(relationshipEntry != null);
            Debug.Assert(relationshipEntry.IsRelationship);
            Debug.Assert(thisEndKey != null);

            if ((EntityKey)relationshipEntry.UsableValues()[0] == thisEndKey)
            {
                return relationshipEntry.UsableValues().DataRecordInfo.FieldMetadata[1].FieldType.Name;
            }
            else if ((EntityKey)relationshipEntry.UsableValues()[1] == thisEndKey)
            {
                return relationshipEntry.UsableValues().DataRecordInfo.FieldMetadata[0].FieldType.Name;
            }
            else
            {
                throw new InvalidOperationException("Neither end of the relationship contains the passed in key.");
            }
        }

        //
        // IRelatedEnd methods
        //

        public static bool IsEntityReference(this IRelatedEnd relatedEnd)
        {
            Type relationshipType = relatedEnd.GetType();
            return (relationshipType.GetGenericTypeDefinition() == typeof(EntityReference<>));
        }

        public static EntityKey GetEntityKey(this IRelatedEnd relatedEnd)
        {
            Debug.Assert(relatedEnd.IsEntityReference());
            Type relationshipType = relatedEnd.GetType();
            PropertyInfo pi = relationshipType.GetProperty("EntityKey");
            return (EntityKey)pi.GetValue(relatedEnd, null);
        }

        public static void SetEntityKey(this IRelatedEnd relatedEnd, EntityKey key)
        {
            Debug.Assert(relatedEnd.IsEntityReference());
            Type relationshipType = relatedEnd.GetType();
            PropertyInfo pi = relationshipType.GetProperty("EntityKey");
            pi.SetValue(relatedEnd, key, null);
        }

        public static bool Contains(this IRelatedEnd relatedEnd, EntityKey key)
        {
            foreach (object relatedObject in relatedEnd)
            {
                Debug.Assert(relatedObject is IEntityWithKey);
                if (((IEntityWithKey)relatedObject).EntityKey == key)
                {
                    return true;
                }
            }
            return false;
        }

        //
        // queries over the context
        //

        public static IEnumerable<IEntityWithKey> GetEntities(this ObjectContext context, EntityState state)
        {
            return from e in context.ObjectStateManager.GetObjectStateEntries(state)
                   where e.IsRelationship == false && e.Entity != null
                   select (IEntityWithKey)e.Entity;
        }

        public static IEnumerable<ObjectStateEntry> GetRelationships(this ObjectContext context, EntityState state)
        {
            return from e in context.ObjectStateManager.GetObjectStateEntries(state)
                   where e.IsRelationship == true
                   select e;
        }

        public static IEnumerable<ObjectStateEntry> GetUnchangedManyToManyRelationships(this ObjectContext context)
        {
            return context.GetRelationships(EntityState.Unchanged)
                .Where(e => ((AssociationType)e.EdmType()).IsManyToMany());
        }

        public static IEnumerable<ObjectStateEntry> GetRelationshipsForKey(this ObjectContext context, EntityKey key, EntityState state)
        {
            return context.GetRelationships(state).Where(e => e.IsRelationshipForKey(key));
        }

        public static IEnumerable<IGrouping<IRelatedEnd, ObjectStateEntry>> GetRelationshipsByRelatedEnd(this ObjectContext context,
            IEntityWithKey entity, EntityState state)
        {
            return from entry in context.GetRelationshipsForKey(entity.EntityKey, state)
                   group entry by ((IEntityWithRelationships)entity).RelationshipManager
                                   .GetRelatedEnd(((AssociationType)(entry.EdmType())).Name,
                                                  entry.OtherEndRole(entity.EntityKey));
        }

        //
        // original values
        //

        // Extension method for the ObjectContext which will create an object instance that is essentially equivalent
        // to the original object that was added or attached to the context before any changes were performed.
        // NOTE: This object will have no relationships--just the original value properties.
        public static object CreateOriginalValuesObject(this ObjectContext context, object source)
        {
            // Get the state entry of the source object
            //     NOTE: For now we require the object to implement IEntityWithKey.
            //           This is something we should be able to relax later.
            Debug.Assert(source is IEntityWithKey);
            EntityKey sourceKey = ((IEntityWithKey)source).EntityKey;
            // This method will throw if the key is null or an entry isn't found to match it.  We
            // could throw nicer exceptions, but this will catch the important invalid cases.
            ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(sourceKey);

            // Return null for added entities & throw an exception for detached ones.  In other cases we can
            // always make a new object with the original values.
            switch (sourceStateEntry.State)
            {
                case EntityState.Added:
                    return null;
                case EntityState.Detached:
                    throw new InvalidOperationException("Can't get original values when detached.");
            }

            // Create target object and add it to the context so that we can easily set properties using 
            // the StateEntry.  Since objects in the added state use temp keys, we know this won't
            // conflict with anything already in the context.
            object target = Activator.CreateInstance(source.GetType());
            string fullEntitySetName = sourceKey.EntityContainerName + "." + sourceKey.EntitySetName;
            context.AddObject(fullEntitySetName, target);
            EntityKey targetKey = context.GetEntityKey(fullEntitySetName, target);
            ObjectStateEntry targetStateEntry = context.ObjectStateManager.GetObjectStateEntry(targetKey);

            // Copy original values from the sourceStateEntry to the targetStateEntry.  This will
            // cause the corresponding properties on the object to be set.
            for (int i = 0; i < sourceStateEntry.OriginalValues.FieldCount; i++)
            {
                // TODO: For best perf we should have a switch on the type here so that we could call 
                // the type-specific methods and avoid boxing.
                targetStateEntry.CurrentValues.SetValue(i, sourceStateEntry.OriginalValues[i]);
            }

            // Detach the object we just created since we only attached it temporarily in order to use 
            // the stateEntry.
            context.Detach(target);

            // Set the EntityKey property on the object (if it implements IEntityWithKey).
            IEntityWithKey targetWithKey = target as IEntityWithKey;
            if (targetWithKey != null)
            {
                targetWithKey.EntityKey = sourceKey;
            }

            return target;
        }

        public static object GetOriginalPropertyValueFromObject(this ObjectContext context, object source, string PropertyName)
        {
            Debug.Assert(source is IEntityWithKey);
            EntityKey sourceKey = ((IEntityWithKey)source).EntityKey;
            ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(sourceKey);

            return sourceStateEntry.OriginalValues[PropertyName];
        }

        public static object CreateOriginalValuesObjectWithReferences(this ObjectContext context, object source)
        {
            object target = context.CreateOriginalValuesObject(source);
            EntityKey srcKey = ((IEntityWithKey)source).EntityKey;
            IEntityWithRelationships sourceWithRelationships = source as IEntityWithRelationships;
            if (sourceWithRelationships == null)
            {
                return target;
            }

            foreach (var relationshipGroup in context.GetRelationshipsByRelatedEnd((IEntityWithKey)target,
                                                                                   EntityState.Unchanged | EntityState.Deleted))
            {
                IRelatedEnd tgtRelatedEnd = (IRelatedEnd)relationshipGroup.Key;
                foreach (ObjectStateEntry srcEntry in relationshipGroup)
                {
                    if (tgtRelatedEnd.IsEntityReference())
                    {
                        tgtRelatedEnd.SetEntityKey(srcEntry.OtherEndKey(srcKey));
                    }
                }
            }

            return target;
        } 
	#endregion

        public static object CreateSerializableVersion(this ObjectContext context, object source)
        {
            Debug.Assert(source is IEntityWithKey);
            Debug.Assert(source is IEditableBusinessObject);

            EntityKey sourceKey = ((IEntityWithKey)source).EntityKey;
            // This method will throw if the key is null or an entry isn't found to match it.  We
            // could throw nicer exceptions, but this will catch the important invalid cases.
            ObjectStateEntry sourceStateEntry = context.ObjectStateManager.GetObjectStateEntry(sourceKey);

            // Return null for added entities & throw an exception for detached ones.  In other cases we can
            // always make a new object with the original values.
            switch (sourceStateEntry.State)
            {
                case EntityState.Added:
                    return source;      // the source is fine as it is!
                case EntityState.Detached:
                    throw new InvalidOperationException("Can't get original values when detached.");
            }

            IEditableBusinessObject eSource = (IEditableBusinessObject)source;

            DbDataRecord record = sourceStateEntry.OriginalValues;

            EntityType t = context.MetadataWorkspace.GetItem<EntityType>(source.GetType().FullName, DataSpace.CSpace);
            foreach (EdmProperty prop in t.Properties)
            {
                object value = record[prop.Name];

                eSource.StorePropertyValueAsLoaded(prop.Name, value);
            }

            return source;
        }
    }



}
